package edu.predatorwars.server.engine;

import java.awt.Point;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import edu.predatorwars.shared.models.Board;
import edu.predatorwars.shared.models.Buff;
import edu.predatorwars.shared.models.Creature;
import edu.predatorwars.shared.models.Location;
import edu.predatorwars.shared.models.ModelUtility;
import edu.predatorwars.shared.models.Move;
import edu.predatorwars.shared.models.Player;
import edu.predatorwars.shared.models.Resource;
import edu.predatorwars.shared.models.Terrain;

/**
 * Maintains the board state, testing and applying new moves as they are received.
 * 
 * @author ESands
 */
public class GameEngine 
{
	/**
	 * The game has been created but not started.
	 * 		Players can be added.
	 * 		Players can select creatures.
	 * 		
	 * 		Players cannot perform moves.
	 * 		The board is not valid.
	 * 
	 * This state is entered when the engine is created.
	 * This state is exited when the engine is told all players have been added.
	 */
	public static final int SETUP_STATE = 0;
	/**
	 * The game is waiting for players to to select their characters.
	 * 		Players can select creatures.
	 *		
	 *		Players cannot be added.
	 * 		Players cannot perform moves.
	 * 		The board is not valid.
	 * 
	 * This state is entered when the engine is told all players have been added.
	 * This state is exited when all players have selected a creature.
	 */
	public static final int CHARACTER_SELECTION_STATE = 1;
	/**
	 * The game is currently is running.
	 * 		Players can perform moves.
	 * 		The board is valid.
	 * 
	 *		Players cannot be added.
	 * 		Players cannot select creatures.
	 * 
	 * This state is entered when all players have selected a creature.
	 * This state is exited when only one player has a living creature.
	 */
	public static final int IN_PROGRESS_STATE = 2;
	/**
	 * The game is finished.
	 *		Players cannot be added.
	 * 		Players cannot select creatures.
	 * 		Players cannot perform moves.
	 * 		The board is not valid.
	 * 
	 * This state is entered when only one player has a living creature.
	 * This state is never exited.
	 */
	public static final int COMPLETED_STATE = 3;

	private Collection<GameListener> listeners = new HashSet<GameListener>();
	private PrintStream log = System.out;
	private List<Creature> availableCreatures;
	private List<Resource> availableResources;
	private List<Location> startLocations;
	private List<Player> players;
	private Board board;

	private int currentPlayer;
	private int moveActionsRemaining;
	private int boardState = SETUP_STATE;



	/**
	 * Creates a game engine without any players.
	 */
	public GameEngine()
	{
		this(new HashSet<Player>());
	}

	/**
	 * Creates a game engine with the given players.
	 * @param players
	 */
	public GameEngine(Collection<Player> players)
	{
		players = new ArrayList<Player>(players);
		selectRandomMap();
		startLocations = ModelUtility.getStartLocations(board);
	}

	/**
	 * Adds the given player to the game. Can only be called during the SETUP_STATE.
	 * @param player
	 */
	public void addPlayer(Player player)
	{
		if(boardState != SETUP_STATE)
			throw new GameException("Players cannot be added well the game is in its current state.");
		if(players.size() == startLocations.size())
			throw new GameException("This board does not support any more players.");
		players.add(player);
	}

	/**
	 * Called if a player leaves or gets disconnected before their character dies
	 * @param player
	 */
	public void removePlayer(Player player)
	{
		if(boardState == IN_PROGRESS_STATE)
		{
			doCreatureDeath(player.getCreature());
		}
		if(boardState == SETUP_STATE || boardState == CHARACTER_SELECTION_STATE) 
		{
			players.remove(player);
		}
	}

	public void doAllPlayersAdded()
	{
		if(boardState != SETUP_STATE)
			throw new GameException("This can not be called well the game is in its current state.");
		if(players.size()<2)
			throw new GameException("Not enough players to continue.");

		players = setTurnOrder(players);

		boardState = CHARACTER_SELECTION_STATE;
		sendGameStateChanged();
	}

	public void doAllCreaturesSelected()
	{
		if(boardState != CHARACTER_SELECTION_STATE)
			throw new GameException("This can not be called well the game is in its current state.");
		for(Player player: players)
			if(player.getCreature() == null)
				throw new GameException("Can not continue, a player has not selected a creature.");

		setStartPositions(players, startLocations);
		distributeResources(board);
		currentPlayer = 0;
		startTurn();
		boardState = IN_PROGRESS_STATE;
		sendGameStateChanged();
	}
	
	private void setStartPositions(List<Player> players, List<Location> startLocations)
	{
		int i;
		for(Player player: players)
		{
			while(true)
			{
				i = (int) (Math.random() * startLocations.size());
				if(startLocations.get(i).getCreature() == null)
				{
					startLocations.get(i).setCreature(player.getCreature());
					break;
				}
			}
		}
	}

	
	private void distributeResources(Board board)
	{
		int x,y,r;
		Location location;
		for(int i = 0; i < board.getResourceCount(); i++)
		{
			while(true)
			{
				x = (int) (Math.random() * board.getWidth());
				y = (int) (Math.random() * board.getHeight());
				location = board.getLocation(x, y);
				if(!location.isStartLocation() && location.getTerrain().isTraversable() 
						&& location.getResource() == null)
				{
					//Pick a random resource and put it here.
					r = (int) (Math.random() * getAvailableResources().size());
					location.setResource(getAvailableResources().get(r));
					break;
				}
			}
		}
	}
	
	/**
	 * Add a listener to monitor the board.
	 * @param listener
	 */
	public void addGameListener(GameListener listener)
	{
		listeners.add(listener);
	}

	private void sendGameStateChanged()
	{
		for(GameListener listener: listeners)
			listener.gameStateChanged(this, boardState);
	}

	/**
	 * Broadcast the board to all listeners.
	 */
	private void sendBoardChanged()
	{
		for(GameListener listener: listeners)
			listener.boardChanged(this, board);
	}

	/**
	 * Randomly selects a map from the data store for this game.
	 */
	private void selectRandomMap()
	{
		board = new Board("Temp Board",new Location[0][0],5);
		//TODO load from data store
	}

	/**
	 * Returns a list of possible creatures.
	 * Loaded from the data store when first called.
	 */
	public List<Creature> getAvailableCreatures()
	{
		if(availableCreatures == null)
		{
			availableCreatures = new ArrayList<Creature>();
			//TODO load from data store
		}
		return availableCreatures;
	}

	/**
	 * Returns a list of possible resources.
	 * Loaded from the data store when first called.
	 */
	private List<Resource> getAvailableResources()
	{
		if(availableResources == null)
		{
			availableResources = new ArrayList<Resource>();
			//TODO load from data store
		}
		return availableResources;
	}
	
	public void setCreature(Player player, String creatureName)
	{
		for(Creature creature: availableCreatures)
			if(creature.getName().equalsIgnoreCase(creatureName))
			{
				player.setCreature(new Creature(creature.getName(),creature.getBaseStats()));
				return;
			}
		throw new GameException("No creature by that name exists.");
	}

	/**
	 * Performs all start of turn actions.
	 */
	private void startTurn()
	{
		Creature creature = players.get(currentPlayer).getCreature();

		//Get the speed stat and round it down, with a minimum value of 1.
		moveActionsRemaining = (int)Math.max(Math.floor(creature.getStat(Creature.STAT_SPEED)),1);

		//Apply any healing buffs to the creature's health.
		for(Buff buff: creature.getBuffs())
		{
			if(buff.getStat().equals(Creature.STAT_HEALTH))
			{
				float health = creature.getStat(Creature.STAT_HEALTH);
				float maxHealth = creature.getStat(Creature.STAT_MAX_HEALTH);
				health = Math.min( health + buff.getValue(), maxHealth);
				creature.setHealth(health);
			}
		}

	}

	/**
	 * Performs all end of turn actions.
	 */
	private void endTurn()
	{
		//end-of-turn damage
		//Update buff status
	}	

	/**
	 * Iff a move is legal, it is executed on the board and true is returned.
	 * Iff a move is not legal, false is returned.
	 * @param m, the move to perform.
	 * @return boolean, true iff the move was executed, false otherwise.
	 */
	public void doMove(Player player, Move m)
	{
		boolean isPass, isAttack, isMovement; 
		Location endLocation = board.getLocation(m.getTargetX(), m.getTargetY());
		Point position = ModelUtility.getPosition(board, player.getCreature());
		isPass = m.getTargetPoint().equals(position);
		isAttack = !isPass && board.getLocation(m.getTargetPoint()).getCreature() != null;
		isMovement = !isAttack && !isPass;

		
		if(player != getCurrentPlayer())
			throw new GameException("Illegal move: it is not this player's turn.");
		if(isMovement && !endLocation.getTerrain().isTraversable())
			throw new GameException("Illegal move: target location is not traversable.");
		if(isMovement && moveActionsRemaining == 0)
			throw new GameException("Illegal move: the current player has exhausted their movement actions this turn.");
		if(ModelUtility.isAdjacent(position,m.getTargetPoint()))
			throw new GameException("Illegal move: the target location is more then 1 grid unit away.");
		
		
		if(isAttack)
		{
			//TODO play out the attack.
			//
		}
		else if(isMovement)
		{
			//TODO move the player's creature to the new location.
		}
		
		log.println(currentPlayer + ": " + m.getTargetX() + "," + m.getTargetY());
		
		if(isMovement)
		{
			sendBoardChanged();
		}
		
		if(isPass || isAttack)
		{
			endTurn();
			currentPlayer++;
			currentPlayer %= players.size();
			startTurn();
		}
	}


	/**
	 * Modifies the board state to reflect the death of a character.
	 * @param creature
	 */
	private void doCreatureDeath(Creature creature)
	{
		//TODO remove creature from location
		//TODO remove player from list
		//TODO if current player who dies, start next turn
		if(players.size() == 0)
		{
			boardState = COMPLETED_STATE;
			sendGameStateChanged();
		}
	}
	
	/**
	 * Returns a list of all the players in this game.
	 * If the game is in the CHARACTER_SELECTION_STATE or has been, 
	 * 		the list will be sorted by the player's turn order.
	 * @return
	 */
	public List<Player> getPlayers()
	{
		return Collections.unmodifiableList(players);
	}

	/**
	 * Returns the player from whom we are expecting the next move.
	 * @return
	 */
	public Player getCurrentPlayer()
	{
		if(boardState != IN_PROGRESS_STATE)
			throw new GameException("There is no current player well the game is in this state.");
		return players.get(currentPlayer);
	}
	
	/**
	 * Returns the player who won.
	 */
	public Player getWinner()
	{
		if(boardState != COMPLETED_STATE)
			throw new GameException("The game is not yet over, there is no winner.");
		return players.get(0);
	}

	/**
	 * Determine the turn order and arrange the players correctly.
	 * @param players, a collection of all the players in the game.
	 * @return
	 */
	private List<Player> setTurnOrder(Collection<Player> players)
	{
		List<Player> temp = new ArrayList<Player>(players);
		List<Player> newPlayers = new ArrayList<Player>();
		Player nextPlayer;
		double p1Speed=0,p2Speed=0;
		do
		{
			nextPlayer = temp.get(0);
			p1Speed = nextPlayer.getCreature().getStat(Creature.STAT_SPEED) + Math.random();
			for(int i =1;i<temp.size();i++)
			{
				p2Speed = temp.get(i).getCreature().getStat(Creature.STAT_SPEED) + Math.random();
				if(p2Speed > p1Speed)
				{
					p1Speed = p2Speed;
					nextPlayer = temp.get(i);
				}
			}
			temp.remove(nextPlayer);
			newPlayers.add(nextPlayer);
		}
		while(!temp.isEmpty());
		return newPlayers;
	}
	
	
	private static boolean testResourceDistribution()
	{
		try
		{
			Location[][] locations = new Location[5][5];
			locations[0][0] = new Location(new Terrain("blah", true,true),true);
			locations[0][4] = new Location(new Terrain("blah", true,true),true);
			locations[4][0] = new Location(new Terrain("blah", true,true),true);
			locations[4][4] = new Location(new Terrain("blah", true,true),true);
			locations[1][1] = new Location(new Terrain("blah", false,true),false);
			locations[2][2] = new Location(new Terrain("blah", false,true),false);
			
			for(int x = 0; x < locations.length; x++)
				for(int y = 0; y < locations.length; y++)
					if(locations[x][y] == null)
						locations[x][y] = new Location(new Terrain("blah", true,true),false);
			
			Board board = new Board("testBoard",locations,4);
			GameEngine ge = new GameEngine();
			ge.distributeResources(board);
			
			int resourceCount = 0;
			for(int x = 0; x < locations.length; x++)
				for(int y = 0; y < locations.length; y++)
					if(locations[x][y].getResource() != null)
						resourceCount++;
			System.out.println(resourceCount);
			if(resourceCount != 4)
				return false;

			if(locations[0][0].getResource() != null)
				return false;
			if(locations[0][4].getResource() != null)
				return false;
			if(locations[4][0].getResource() != null)
				return false;
			if(locations[4][4].getResource() != null)
				return false;

			if(locations[1][1].getResource() != null)
				return false;
			if(locations[2][2].getResource() != null)
				return false;
			
			return true;
		} 
		catch (Exception e) 
		{
e.printStackTrace();
			return false;
		}
	}
	
	
	private static boolean testSetTurnOrder()
	{
		try {
			GameEngine ge = new GameEngine();

			List<Player> players = new ArrayList<Player>();
			Player player = new Player(null);
			Map<String,Float> stats = new HashMap<String,Float>();
			stats.put(Creature.STAT_SPEED, 5f);
			Creature creature = new Creature("Test1Speed5",stats);
			player.setCreature(creature);
			players.add(player);

			player = new Player(null);
			stats = new HashMap<String,Float>();
			stats.put(Creature.STAT_SPEED, 4f);
			creature = new Creature("Test2Speed4",stats);
			player.setCreature(creature);
			players.add(player);

			player = new Player(null);
			stats = new HashMap<String,Float>();
			stats.put(Creature.STAT_SPEED, 6f);
			creature = new Creature("Test3Speed6",stats);
			player.setCreature(creature);
			players.add(player);

			player = new Player(null);
			stats = new HashMap<String,Float>();
			stats.put(Creature.STAT_SPEED, 3f);
			creature = new Creature("Test4Speed3",stats);
			player.setCreature(creature);
			players.add(player);

			player = new Player(null);
			stats = new HashMap<String,Float>();
			stats.put(Creature.STAT_SPEED, 3f);
			creature = new Creature("Test5Speed3",stats);
			player.setCreature(creature);
			players.add(player);
			

			player = new Player(null);
			stats = new HashMap<String,Float>();
			stats.put(Creature.STAT_SPEED, 5f);
			creature = new Creature("Test6Speed5",stats);
			player.setCreature(creature);
			players.add(player);
			
			players = ge.setTurnOrder(players);
			float lastSpeed = 9999f;
			for(int i =0;i<players.size();i++)
			{
				float speed = players.get(i).getCreature().getStat(Creature.STAT_SPEED);
				if(speed > lastSpeed)
					return false;
				lastSpeed = speed;
			}
			return true;
		} 
		catch (Exception e) 
		{
			return false;
		}
	}
	
	public static void main(String[] args)
	{
		System.out.println(testSetTurnOrder());
		System.out.println(testResourceDistribution());
	}
}